package cz.matasek.dbLogger.database;

import cz.matasek.dbLogger.Constant;
import cz.matasek.dbLogger.sql.ReplacementKeyword;
import cz.matasek.dbLogger.LoggingAvailability;
import cz.matasek.dbLogger.Settings;
import cz.matasek.dbLogger.exception.DatabaseErrorException;
import cz.matasek.dbLogger.exception.LoggingUnavailableException;
import cz.matasek.dbLogger.exception.HistoryLoggingInactive;
import cz.matasek.dbLogger.exception.JdbcDriverMissingException;
import cz.matasek.dbLogger.exception.NoConnectionEstablishedException;
import cz.matasek.dbLogger.exception.ObjectAlreadyExistsException;
import cz.matasek.dbLogger.exception.ObjectDoesNotExistException;
import cz.matasek.dbLogger.sql.SQLKeyword;
import cz.matasek.dbLogger.sql.SQLStatement;
import cz.matasek.dbLogger.sql.SQLStatementType;
import cz.matasek.dbLogger.model.TriggeringEvents;
import cz.matasek.dbLogger.Util;
import cz.matasek.dbLogger.model.DateTime;
import cz.matasek.dbLogger.model.TriggeringEvents.Event;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * An abstract implementation of
 * the {@link cz.matasek.dbLogger.database.DatabaseConnection} object.
 *
 * @author Luboš Matásek
 */
public abstract class AbstractDatabaseConnection implements DatabaseConnection {

    /**
     * Database platform.
     */
    private DatabasePlatform databasePlatform;
    /**
     * Name of the database user.
     */
    private String username;
    /**
     * Password of the database user.
     */
    private String password;
    /**
     * Database host.
     */
    protected String host;
    /**
     * Database name.
     */
    protected String databaseName;
    /**
     * Database port.
     */
    protected String port;
    /**
     * Connection to the database.
     */
    private Connection conn;
    /**
     * Prefix of all logging object.
     */
    private String loggingObjectsPrefix;
    /**
     * Logging status.
     */
    private LoggingAvailability loggingStatus;

    /**
     * Creates a new <code>AbstractDatabaseConnection</code> object with
     * the specified parameters.
     *
     * @param databasePlatform database platform
     * @param username database username
     * @param password database password
     * @param host database host
     * @param databaseName database name
     * @param port database port
     */
    public AbstractDatabaseConnection(DatabasePlatform databasePlatform,
            String username, String password,
            String host, String databaseName, String port) {
        this.databasePlatform = databasePlatform;
        this.username = username;
        this.password = password;
        this.host = host;
        this.databaseName = databaseName;
        this.port = port;
    }

    @Override
    public final Connection getConnection() {
        return this.conn;
    }

    @Override
    public final DatabasePlatform getPlatform() {
        return this.databasePlatform;
    }

    @Override
    public final String getPlatformName() {
        return this.databasePlatform.toString();
    }

    @Override
    public final LoggingAvailability getLoggingAvailability() {
        return this.loggingStatus;
    }

    @Override
    public final String getLoggingObjectsPrefix() {
        return this.loggingObjectsPrefix;
    }

    @Override
    public final String getDatabaseDescription() {
        try {
            return username + "@" + host + ":" + port + "\n" +
                    this.conn.getMetaData().getDatabaseProductName() + " " +
                    this.conn.getMetaData().getDatabaseProductVersion();
        } catch (SQLException ex) {
            return new String();
        }
    }

    @Override
    public abstract String getJdbcDriverName();

    /**
     * Returns this database connection string for the JDBC driver.
     *
     * @return database connection string for the JDBC driver
     */
    protected abstract String getJdbcConnectionString();

    @Override
    public final void connect() throws NoConnectionEstablishedException {
        // checks existence of the JDBC driver
        try {
            Class.forName(this.getJdbcDriverName());
        } catch (ClassNotFoundException ex) {
            throw new JdbcDriverMissingException(this);
        }

        // tries connect to the database
        try {
            this.conn = DriverManager.getConnection(
                    this.getJdbcConnectionString(), this.username, this.password);
        } catch (SQLException ex) {
            throw new NoConnectionEstablishedException(ex);
        }

        this.readMetadata();
    }

    @Override
    public final void disconnect() {
        // checks null value
        if (this.conn == null) {
            return;
        }

        // disconnects from the database
        try {
            this.conn.close();
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }

        this.clearMetadata();
    }

    /**
     * Initializes important database metadata.
     * Checks if DbLogger is enabled in the database and eventually gets
     * DbLogger prefix. Also reads list of tables present in the database.
     */
    protected final void readMetadata() {
        // reads logging prefix
        if (this.hasPrefixFunction()) {
            this.loggingObjectsPrefix = this.getPrefix();
        } else {
            this.loggingObjectsPrefix = null;
        }

        // sets logging status
        if (this.hasLoggingSequence()) {
            this.loggingStatus = LoggingAvailability.AVAILABLE;
        } else {
            this.loggingStatus = LoggingAvailability.UNAVAILABLE;
        }
    }

    /**
     * Clears saved database metadata.
     * It means logging status, prefix etc.
     */
    protected final void clearMetadata() {
        this.loggingStatus = null;
        this.loggingObjectsPrefix = null;
    }

    @Override
    public final void createLoggingEnvironment(String prefix) {
        try {
            this.loggingObjectsPrefix = prefix;

            // starts transaction
            this.conn.setAutoCommit(false);

            // creates all necessary objects
            this.createPrefixFunction(prefix);
            this.createLoggingSequence();

            // commits transaction
            this.conn.commit();

            // sets auto commiting
            this.conn.setAutoCommit(true);
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        } finally {
            // sets database metadate
            this.readMetadata();
        }
    }

    @Override
    public final void dropLoggingEnvironment(boolean cascaded) {
        try {
            // starts transaction
            this.conn.setAutoCommit(false);

            if (cascaded) {
                for (String tableName : getTables()) {
                    if (hasHistoryTrigger(tableName)) {
                        stopLoggingHistory(tableName);
                    }

                    if (hasLoggingTrigger(tableName)) {
                        dropLoggingTrigger(tableName);
                    }
                }

                if (hasLoggingTable()) {
                    dropLoggingTable();
                }
            }

            // drops all logging objects
            if (this.hasLoggingSequence()) {
                this.dropLoggingSequence();
            }
            if (this.hasPrefixFunction()) {
                this.dropPrefixFunction();
            }

            // commits transaction
            this.conn.commit();

            // sets auto commiting
            this.conn.setAutoCommit(true);
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        } finally {
            // sets database metadate
            this.readMetadata();
        }
    }

    @Override
    public boolean hasFunction(String functionName) {
        try {
            // creates SQL to find out if function exists
            SQLStatement sqlHasPrefixFunction = new SQLStatement(
                    this, SQLStatementType.HAS_FUNCTION);

            // prepares statement
            sqlHasPrefixFunction.prepareStatement();

            // sets function name
            sqlHasPrefixFunction.setString(1, functionName);

            // executes query
            ResultSet resultHasPrefixFunction =
                    sqlHasPrefixFunction.executeQuery();

            // retrieves number of functions
            int functionsCount = 0;
            if (resultHasPrefixFunction.next()) {
                functionsCount = resultHasPrefixFunction.getInt(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlHasPrefixFunction.close();


            // send result
            if (functionsCount == 1) {
                return true;
            } else {
                return false;
            }
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public boolean hasSequence(String sequenceName) {
        try {
            // creates SQL to find out if sequence exists
            SQLStatement sqlHasSequence = new SQLStatement(
                    this, SQLStatementType.HAS_SEQUENCE);

            // prepares statement
            sqlHasSequence.prepareStatement();

            // sets function name
            sqlHasSequence.setString(1, sequenceName);

            // executes query
            ResultSet resultHasSequence = sqlHasSequence.executeQuery();

            // retrieves number of sequences
            int sequencesCount = 0;
            if (resultHasSequence.next()) {
                sequencesCount = resultHasSequence.getInt(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlHasSequence.close();


            // send result
            if (sequencesCount == 1) {
                return true;
            } else {
                return false;
            }
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public boolean hasTable(String tableName) {
        try {
            // creates SQL to find out if logging table exists
            SQLStatement sqlHasLoggingTable =
                    new SQLStatement(this, SQLStatementType.HAS_TABLE);

            // prepares statement
            sqlHasLoggingTable.prepareStatement();

            // sets logging table name
            sqlHasLoggingTable.setString(1, tableName);

            // executes query
            ResultSet resultHasLoggingTable = sqlHasLoggingTable.executeQuery();

            // retrieves number of functions
            int tablesCount = 0;
            if (resultHasLoggingTable.next()) {
                tablesCount = resultHasLoggingTable.getInt(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlHasLoggingTable.close();


            // send result
            if (tablesCount == 1) {
                return true;
            } else {
                return false;
            }
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public boolean hasTrigger(String triggerName) {
        try {
            // creates SQL to find out if logging trigger exists
            SQLStatement sqlHasLoggingTrigger =
                    new SQLStatement(this, SQLStatementType.HAS_TRIGGER);

            // prepares statement
            sqlHasLoggingTrigger.prepareStatement();

            // sets logging trigger name
            sqlHasLoggingTrigger.setString(1, triggerName);

            // executes query
            ResultSet resultHasLoggingTrigger =
                    sqlHasLoggingTrigger.executeQuery();

            // retrieves number of triggers
            int triggersCount = 0;
            if (resultHasLoggingTrigger.next()) {
                triggersCount = resultHasLoggingTrigger.getInt(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlHasLoggingTrigger.close();


            // send result
            if (triggersCount == 1) {
                return true;
            } else {
                return false;
            }
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public boolean hasColumn(String tableName, String columnName) {
        try {
            // creates SQL to find out if the given column exists
            SQLStatement sqlHasColumn =
                    new SQLStatement(this, SQLStatementType.HAS_COLUMN);

            // prepares statement
            sqlHasColumn.prepareStatement();

            // sets the table name and the column name
            sqlHasColumn.setString(1, tableName);
            sqlHasColumn.setString(2, columnName);

            // executes query
            ResultSet resultHasColumn = sqlHasColumn.executeQuery();

            // retrieves number of columns
            int columnsCount = 0;
            if (resultHasColumn.next()) {
                columnsCount = resultHasColumn.getInt(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlHasColumn.close();


            // send result
            if (columnsCount == 1) {
                return true;
            } else {
                return false;
            }
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    /**
     * Returns name of the prefix function.
     *
     * @return name of the prefix function
     */
    protected final String getPrefixFunctionName() {
        return Settings.get(Settings.Key.PREFIX_FUNCTION_NAME);
    }

    /**
     * Retrieves whether prefix function exists in the database.
     * Prefix function returns prefix for all objects belonging to DbLogger
     * application. Name of the function is saved in
     * {@link cz.matasek.dbLogger.Settings} class.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.DatabaseErrorException}
     * if database error occurs.
     *
     * @return true if prefix function exists; false otherwise
     */
    protected final boolean hasPrefixFunction() {
        return this.hasFunction(this.getPrefixFunctionName());
    }

    /**
     * Creates prefix function.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectAlreadyExistsException}
     * if prefix function already exists.
     *
     * @param prefix prefix used for application objects
     */
    protected void createPrefixFunction(String prefix) {
        // checks existence of the prefix function
        if (this.hasPrefixFunction()) {
            throw new ObjectAlreadyExistsException(
                    DatabaseObjectType.FUNCTION, this.getPrefixFunctionName());
        }


        // creates SQL to create prefix function
        SQLStatement sqlCreatePrefixFunction = new SQLStatement(
                this, SQLStatementType.CREATE_PREFIX_FUNCTION);

        // makes keyword replacements
        sqlCreatePrefixFunction.replaceKeyword(
                ReplacementKeyword.PREFIX_FUNCTION_NAME, this.getPrefixFunctionName());
        sqlCreatePrefixFunction.replaceKeyword(
                ReplacementKeyword.PREFIX, prefix);

        // performs query
        sqlCreatePrefixFunction.executeUpdate();
    }

    /**
     * Drops prefix function.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectDoesNotExistException}
     * if prefix function does not exist.
     */
    protected void dropPrefixFunction() {
        // checks existance of the prefix function
        if (!this.hasPrefixFunction()) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.FUNCTION, this.getPrefixFunctionName());
        }


        // creates SQL to drop prefix function
        SQLStatement sqlDropPrefixFunction = new SQLStatement(
                this, SQLStatementType.DROP_PREFIX_FUNCTION);

        // makes keyword replacements
        sqlDropPrefixFunction.replaceKeyword(
                ReplacementKeyword.PREFIX_FUNCTION_NAME, this.getPrefixFunctionName());

        // performs query
        sqlDropPrefixFunction.executeUpdate();
    }

    @Override
    public String getPrefix() {
        // checks existance of the prefix function
        if (!this.hasPrefixFunction()) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.FUNCTION, this.getPrefixFunctionName());
        }


        try {
            // creates SQL to get prefix
            SQLStatement sqlGetPrefix = new SQLStatement(
                    this, SQLStatementType.GET_PREFIX);

            // makes keyword replacements
            sqlGetPrefix.replaceKeyword(
                    ReplacementKeyword.PREFIX_FUNCTION_NAME, this.getPrefixFunctionName());

            // retrieves prefix
            ResultSet resultGetPrefix = sqlGetPrefix.executeQuery();

            String result;
            if (resultGetPrefix.next()) {
                result = resultGetPrefix.getString(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlGetPrefix.close();


            // sends result
            return result;
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    /**
     * Returns sequence name for this database.
     *
     * @return sequence name
     */
    protected final String getLoggingSequenceName() {
        return loggingObjectsPrefix + Settings.get(Settings.Key.SEQUENCE_SUFFIX);
    }

    /**
     * Retrieves whether the logging sequence exists in the database.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.DatabaseErrorException}
     * if database error occurs.
     *
     * @return true if logging sequence exists; false otherwise
     */
    protected final boolean hasLoggingSequence() {
        return this.hasSequence(this.getLoggingSequenceName());
    }

    /**
     * Creates sequence used in the logging table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectAlreadyExistsException}
     * if sequence already exists.
     */
    protected void createLoggingSequence() {
        // checks existence of the sequence
        if (this.hasLoggingSequence()) {
            throw new ObjectAlreadyExistsException(
                    DatabaseObjectType.SEQUENCE, this.getLoggingSequenceName());
        }


        // creates SQL to create sequence
        SQLStatement sqlCreateSequence =
                new SQLStatement(this, SQLStatementType.CREATE_LOGGING_SEQUENCE);

        // makes keyword replacements
        sqlCreateSequence.replaceKeyword(
                ReplacementKeyword.SEQUENCE_NAME, this.getLoggingSequenceName());

        // performs query
        sqlCreateSequence.executeUpdate();
    }

    /**
     * Drops sequence used in the logging table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectDoesNotExistException}
     * if sequence does not exist.
     */
    protected void dropLoggingSequence() {
        // checks existence of the sequence
        if (!this.hasLoggingSequence()) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.SEQUENCE, this.getLoggingSequenceName());
        }


        // creates SQL to drop sequence
        SQLStatement sqlDropSequence =
                new SQLStatement(this, SQLStatementType.DROP_LOGGING_SEQUENCE);

        // makes keyword replacements
        sqlDropSequence.replaceKeyword(
                ReplacementKeyword.SEQUENCE_NAME, this.getLoggingSequenceName());

        // performs query
        sqlDropSequence.executeUpdate();
    }

    @Override
    public final String getLoggingTableName() {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        return loggingObjectsPrefix + Settings.get(Settings.Key.LOGGING_TABLE_SUFFIX);
    }

    @Override
    public final boolean hasLoggingTable() {
        return this.hasTable(this.getLoggingTableName());
    }

    @Override
    public void createLoggingTable() {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (this.hasTable(this.getLoggingTableName())) {
            throw new ObjectAlreadyExistsException(
                    DatabaseObjectType.TABLE, this.getLoggingTableName());
        }


        // creates SQL to create logging table
        SQLStatement sqlCreateLoggingTable = new SQLStatement(
                this, SQLStatementType.CREATE_LOGGING_TABLE);

        // makes keyword replacements
        sqlCreateLoggingTable.replaceKeyword(
                ReplacementKeyword.LOGGING_TABLE_NAME, this.getLoggingTableName());
        sqlCreateLoggingTable.replaceKeyword(
                ReplacementKeyword.SEQUENCE_NAME, this.getLoggingSequenceName());

        // performs query
        sqlCreateLoggingTable.executeUpdate();
    }

    @Override
    public void dropLoggingTable() {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasTable(this.getLoggingTableName())) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TABLE, this.getLoggingTableName());
        }


        // creates SQL to drop logging table
        SQLStatement sqlDropLoggingTable = new SQLStatement(
                this, SQLStatementType.DROP_LOGGING_TABLE);

        // makes keyword replacements
        sqlDropLoggingTable.replaceKeyword(
                ReplacementKeyword.LOGGING_TABLE_NAME, this.getLoggingTableName());

        // performs query
        sqlDropLoggingTable.executeUpdate();
    }

    @Override
    public Collection<String> getTables() {
        try {
            // creates SQL to get all tables
            SQLStatement sqlGetTables =
                    new SQLStatement(this, SQLStatementType.GET_TABLES);

            // executes query
            ResultSet resultGetTables = sqlGetTables.executeQuery();

            // retrieves list of tables
            Set<String> databaseTables = new HashSet<String>();
            while (resultGetTables.next()) {
                String tableName = resultGetTables.getString(1);

                databaseTables.add(tableName);
            }

            // closes statement
            sqlGetTables.close();


            // send result
            return databaseTables;
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public Map<String, String> getColumns(String tableName) {
        try {
            // creates SQL to get all columns of specified table
            SQLStatement sqlGetColumns =
                    new SQLStatement(this, SQLStatementType.GET_COLUMNS);

            // prepares statement
            sqlGetColumns.prepareStatement();

            // binds variables
            sqlGetColumns.setString(1, tableName);

            // executes query
            ResultSet resultGetColumns = sqlGetColumns.executeQuery();

            // retrieves list of columns
            Map<String, String> columns = new HashMap<String, String>();
            while (resultGetColumns.next()) {
                columns.put(
                        resultGetColumns.getString(1),
                        resultGetColumns.getString(2));
            }

            // closes statement
            sqlGetColumns.close();


            // send result
            return columns;
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public Map<String, String> getNonLoggingColumns(String tableName) {
        if (loggingObjectsPrefix == null) {
            return this.getColumns(tableName);
        }

        Map<String, String> nonLoggingColumns = new HashMap<String, String>();
        for (Entry<String, String> column : getColumns(tableName).entrySet()) {
            String columnName = column.getKey();
            String columnDataType = column.getValue();
            if (!Util.startsWithIgnoreCase(columnName, loggingObjectsPrefix)) {
                nonLoggingColumns.put(columnName, columnDataType);
            }
        }

        return nonLoggingColumns;
    }

    @Override
    public Collection<String> getLoggedColumns(String tableName) {
        if (!hasLoggingTrigger(tableName)) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TRIGGER, getLoggingTriggerName(tableName));
        }

        String triggerBody;
        try {
            // creates SQL to get all logged columns of specified table
            SQLStatement sqlGetLoggedColumns =
                    new SQLStatement(this, SQLStatementType.GET_LOGGED_COLUMNS);

            // prepares statement
            sqlGetLoggedColumns.prepareStatement();

            // binds variables
            sqlGetLoggedColumns.setString(1, getLoggingTriggerName(tableName));

            // executes query
            ResultSet resultGetLoggedColumns = sqlGetLoggedColumns.executeQuery();

            // retrieves body of the trigger
            if (resultGetLoggedColumns.next()) {
                triggerBody = resultGetLoggedColumns.getString(1);
            } else {
                assert false; // SQL query should return exactly 1 row
                throw new DatabaseErrorException(
                        DatabaseErrorException.MESSAGE_NO_ROWS_RETURNED);
            }

            // closes statement
            sqlGetLoggedColumns.close();
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }


        // retrieves list of logged columns
        List<String> loggedColumns = new ArrayList<String>();
        for (String columnName : getColumns(tableName).keySet()) {
            if (Util.containsIgnoreCase(triggerBody,
                    "xmlattributes('" + columnName + "'")) {
                loggedColumns.add(columnName);
            }
        }


        // send result
        return loggedColumns;
    }

    @Override
    public void createLoggingTrigger(String tableName, TriggeringEvents events,
            Collection<String> loggedColumnNames) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasLoggingTable()) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TABLE, this.getLoggingTableName());
        }


        // creates SQL to create logging trigger
        SQLStatement sqlCreateLoggingTrigger =
                new SQLStatement(this, SQLStatementType.CREATE_LOGGING_TRIGGER);

        // makes keyword replacements
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.LOGGING_TABLE_NAME, this.getLoggingTableName());
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.TABLE_NAME, tableName);
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.SEQUENCE_NAME, this.getLoggingSequenceName());
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.TRIGGER_EVENTS, events.getSQL());
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.TRIGGER_NAME, this.getLoggingTriggerName(tableName));
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.LOGGING_TRIGGER_LOOP_OLD,
                this.getLoggingTriggerLoop(loggedColumnNames, SQLKeyword.OLD));
        sqlCreateLoggingTrigger.replaceKeyword(
                ReplacementKeyword.LOGGING_TRIGGER_LOOP_NEW,
                this.getLoggingTriggerLoop(loggedColumnNames, SQLKeyword.NEW));


        // performs query
        sqlCreateLoggingTrigger.executeUpdate();
    }

    @Override
    public void dropLoggingTrigger(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasLoggingTrigger(tableName)) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TRIGGER, this.getLoggingTriggerName(tableName));
        }


        // creates SQL to drop logging trigger
        SQLStatement sqlDropLoggingTrigger =
                new SQLStatement(this, SQLStatementType.DROP_LOGGING_TRIGGER);

        // makes keyword replacements
        sqlDropLoggingTrigger.replaceKeyword(
                ReplacementKeyword.TRIGGER_NAME, this.getLoggingTriggerName(tableName));

        // performs query
        sqlDropLoggingTrigger.executeUpdate();
    }

    /**
     * Returns logging trigger name for the specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     *
     * @param tableName
     * @return logging trigger name
     */
    protected final String getLoggingTriggerName(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        return loggingObjectsPrefix + tableName +
                Settings.get(Settings.Key.LOGGING_TRIGGER_SUFFIX);
    }

    @Override
    public final boolean hasLoggingTrigger(String tableName) {
        return this.hasTrigger(this.getLoggingTriggerName(tableName));
    }

    @Override
    public TriggeringEvents getTriggerEvents(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasLoggingTrigger(tableName)) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TRIGGER, this.getLoggingTriggerName(tableName));
        }


        // creates SQL to get trigger events
        SQLStatement sqlGetTriggerEvents =
                new SQLStatement(this, SQLStatementType.GET_TRIGGER_EVENTS);

        // prepares statement
        sqlGetTriggerEvents.prepareStatement();

        // sets bind variables
        sqlGetTriggerEvents.setString(1, this.getLoggingTriggerName(tableName));


        // executes query
        ResultSet resultGetTriggerEvents = sqlGetTriggerEvents.executeQuery();

        // retrieves list of triggering events
        TriggeringEvents events = new TriggeringEvents();

        try {
            while (resultGetTriggerEvents.next()) {
                String event = resultGetTriggerEvents.getString(1);

                if (event.toUpperCase().contains(Event.INSERT.toString())) {
                    events.setEvent(Event.INSERT);
                }
                if (event.toUpperCase().contains(Event.UPDATE.toString())) {
                    events.setEvent(Event.UPDATE);
                }
                if (event.toUpperCase().contains(Event.DELETE.toString())) {
                    events.setEvent(Event.DELETE);
                }


                // retrieves list of on update columns
                if (event.contains(" " + SQLKeyword.OF + " ")) {
                    Set<String> onUpdateColumns = new HashSet<String>();
                    String onUpdateColumnsString = event.replaceAll(".* " + SQLKeyword.OF + " ", "").replaceAll(" O[RN].*", "");
                    for (String columnName : getColumns(tableName).keySet()) {
                        if (Util.containsIgnoreCase(onUpdateColumnsString, columnName)) {
                            onUpdateColumns.add(columnName);
                        }
                    }

                    events.setOnUpdateColumns(onUpdateColumns);
                }
            }
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }


        // closes statement
        sqlGetTriggerEvents.close();

        return events;
    }

    /**
     * Returns PL/SQL code for logging all given columns.
     *
     * @param columns columns to be logged
     * @param type
     * @return PL/SQL code for logging column values
     */
    protected String getLoggingTriggerLoop(Collection<String> columns, SQLKeyword type) {
        // sorts columns in the alphabetical order
        List<String> sortedColumns = new ArrayList<String>(columns);
        //sortedColumns.addAll(columns);
        Collections.sort(sortedColumns);

        String result = new String();
        for (String columnName : sortedColumns) {
//            // unsupported column are not logged
//            if (!column.getDataType().isLoggeable()) {
//                continue;
//            }

            // skips logger columns
            if (Util.startsWithIgnoreCase(columnName, this.loggingObjectsPrefix)) {
                continue;
            }

            // creates logging trigger loop
            SQLStatement sqlLoggingTriggerLoop = new SQLStatement(
                    this, SQLStatementType.LOGGING_TRIGGER_LOOP);

            // makes replacements
            sqlLoggingTriggerLoop.replaceKeyword(
                    ReplacementKeyword.COLUMN_NAME, columnName);
            sqlLoggingTriggerLoop.replaceKeyword(
                    ReplacementKeyword.TYPE, type.toString());

            // appends code for every column
            result += sqlLoggingTriggerLoop.getRawSQL();
        }


        // sends result
        return result;
    }

    /**
     * Creates columns for logging full history in the specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     *
     * @param tableName table to be logged
     */
    protected void createHistoryColumns(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (this.hasHistoryColumns(tableName)) {
            throw new ObjectAlreadyExistsException(
                    DatabaseObjectType.COLUMN,
                    "history columns");
        }


        // creates SQL to create history columns in the specified table
        SQLStatement sqlCreateHistoryColumns = new SQLStatement(
                this, SQLStatementType.CREATE_HISTORY_COLUMNS);

        // makes keyword replacements
        sqlCreateHistoryColumns.replaceKeyword(
                ReplacementKeyword.PREFIX, this.loggingObjectsPrefix);
        sqlCreateHistoryColumns.replaceKeyword(
                ReplacementKeyword.SEQUENCE_NAME, this.getLoggingSequenceName());
        sqlCreateHistoryColumns.replaceKeyword(
                ReplacementKeyword.TABLE_NAME, tableName);

        // performs query
        sqlCreateHistoryColumns.executeUpdate();

        // update metadata
        this.readMetadata();
    }

    /**
     * Drops history logging columns from the specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     * Can throw {@link cz.matasek.dbLogger.exception.HistoryLoggingInactive}
     * if full history logging is inactive in the table.
     *
     * @param tableName table, where columns are to be deleted
     */
    protected void dropHistoryColumns(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasHistoryColumns(tableName)) {
            throw new HistoryLoggingInactive(tableName);
        }


        // create SQL to drop history columns from the specified table
        SQLStatement sqlDropHistoryColumns = new SQLStatement(
                this, SQLStatementType.DROP_HISTORY_COLUMNS);

        // makes keyword replacements
        sqlDropHistoryColumns.replaceKeyword(
                ReplacementKeyword.PREFIX, this.loggingObjectsPrefix);
        sqlDropHistoryColumns.replaceKeyword(
                ReplacementKeyword.TABLE_NAME, tableName);

        // performs query
        sqlDropHistoryColumns.executeUpdate();

        // update metadata
        this.readMetadata();
    }

    @Override
    public final String[] getHistoryColumnsNames() {
        String[] historyColumnsNames =
                new String[Constant.HISTORY_COLUMN_NAMES.length];

        for (int i = 0; i < historyColumnsNames.length; i++) {
            historyColumnsNames[i] =
                    this.loggingObjectsPrefix + Constant.HISTORY_COLUMN_NAMES[i];
        }

        return historyColumnsNames;
    }

    /**
     * Retrieves whether the specified table has columns for logging history.
     *
     * @param tableName
     * @return true if the table has columns for logging history; false
     * otherwise
     */
    protected final boolean hasHistoryColumns(String tableName) {
        for (String columnName : this.getHistoryColumnsNames()) {
            if (!this.hasColumn(tableName, columnName)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public final String getHistoryTableName(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        return loggingObjectsPrefix + tableName +
                Settings.get(Settings.Key.HISTORY_TABLE_SUFFIX);
    }

    /**
     * Creates history table for the specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectAlreadyExistsException}
     * if history table already exists.
     * Can throw {@link cz.matasek.dbLogger.exception.DatabaseErrorException}
     * if database error occurs.
     *
     * @param tableName table, history table is to be created for
     */
    protected void createHistoryTable(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (this.hasTable(this.getHistoryTableName(tableName))) {
            throw new ObjectAlreadyExistsException(
                    DatabaseObjectType.TABLE, this.getHistoryTableName(tableName));
        }

        if (!this.hasHistoryColumns(tableName)) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.COLUMN, "history columns");
        }


        // creates SQL to create history table
        SQLStatement sqlCreateHistoryTable =
                new SQLStatement(this, SQLStatementType.CREATE_HISTORY_TABLE);

        // creates part of SQL containing column names and data types
        List<String> columnsDefinition = new ArrayList<String>();
        for (Entry<String, String> column : getColumns(tableName).entrySet()) {
            String columnName = column.getKey();
            String columnDataType = column.getValue();

            columnsDefinition.add(columnName + " " + columnDataType);
        }

        String sqlColumnsDefinition = Util.implode(
                Constant.COMMA, columnsDefinition.toArray());

        // makes keyword replacements
        sqlCreateHistoryTable.replaceKeyword(
                ReplacementKeyword.PREFIX, this.loggingObjectsPrefix);
        sqlCreateHistoryTable.replaceKeyword(
                ReplacementKeyword.HISTORY_TABLE_NAME, this.getHistoryTableName(tableName));
        sqlCreateHistoryTable.replaceKeyword(
                ReplacementKeyword.HISTORY_TABLE_COLUMNS, sqlColumnsDefinition);


        // performs query
        sqlCreateHistoryTable.executeUpdate();
    }

    /**
     * Drops history table for the specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectDoesNotExistException}
     * if history table does not exist.
     * Can throw {@link cz.matasek.dbLogger.exception.DatabaseErrorException}
     * if database error occurs.
     *
     * @param tableName table, whose history table is to be deleted
     */
    protected void dropHistoryTable(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasTable(this.getHistoryTableName(tableName))) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TABLE, this.getHistoryTableName(tableName));
        }


        // creates SQL to create history table
        SQLStatement sqlDropHistoryTable =
                new SQLStatement(this, SQLStatementType.DROP_HISTORY_TABLE);

        // makes keyword replacements
        sqlDropHistoryTable.replaceKeyword(
                ReplacementKeyword.HISTORY_TABLE_NAME, this.getHistoryTableName(tableName));


        // performs query
        sqlDropHistoryTable.executeUpdate();
    }

    @Override
    public final boolean hasHistoryTable(String tableName) {
        return this.hasTable(this.getHistoryTableName(tableName));
    }

    /**
     * Returns history trigger name for the specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     *
     * @param tableName name of the table
     * @return history trigger name
     */
    protected final String getHistoryTriggerName(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        return loggingObjectsPrefix + tableName +
                Settings.get(Settings.Key.HISTORY_TRIGGER_SUFFIX);
    }

    /**
     * Retrieves whether the specified table has history trigger.
     *
     * @param tableName name of the table
     * @return true if the table has the history trigger; false otherwise
     */
    protected final boolean hasHistoryTrigger(String tableName) {
        return this.hasTrigger(this.getHistoryTriggerName(tableName));
    }

    /**
     * Creates history trigger for specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectDoesNotExistException}
     * if history table does not exist.
     * Can throw {@link cz.matasek.dbLogger.exception.DatabaseErrorException}
     * if database error occurs.
     *
     * @param tableName table to be logged
     */
    protected void createHistoryTrigger(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasHistoryTable(tableName)) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TABLE, this.getHistoryTableName(tableName));
        }


        // creates SQL to create history trigger
        SQLStatement sqlCreateHistoryTrigger =
                new SQLStatement(this, SQLStatementType.CREATE_HISTORY_TRIGGER);

        // makes keyword replacements
        sqlCreateHistoryTrigger.replaceKeyword(
                ReplacementKeyword.PREFIX, this.loggingObjectsPrefix);
        sqlCreateHistoryTrigger.replaceKeyword(
                ReplacementKeyword.TRIGGER_NAME, this.getHistoryTriggerName(tableName));
        sqlCreateHistoryTrigger.replaceKeyword(
                ReplacementKeyword.TABLE_NAME, tableName);
        sqlCreateHistoryTrigger.replaceKeyword(
                ReplacementKeyword.SEQUENCE_NAME, this.getLoggingSequenceName());
        sqlCreateHistoryTrigger.replaceKeyword(
                ReplacementKeyword.HISTORY_TRIGGER,
                this.getHistoryTriggerLoop(tableName));


        // performs query
        sqlCreateHistoryTrigger.executeUpdate();
    }

    /**
     * Drops history trigger for specified table.
     *
     * Can throw {@link cz.matasek.dbLogger.exception.LoggingUnavailableException}
     * if logging is inactive in the database.
     * Can throw {@link cz.matasek.dbLogger.exception.ObjectDoesNotExistException}
     * if history table does not exist.
     * Can throw {@link cz.matasek.dbLogger.exception.DatabaseErrorException}
     * if database error occurs.
     *
     * @param tableName table to be logged
     */
    protected void dropHistoryTrigger(String tableName) {
        if (this.loggingStatus == LoggingAvailability.UNAVAILABLE) {
            throw new LoggingUnavailableException();
        }

        if (!this.hasHistoryTrigger(tableName)) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TRIGGER, this.getHistoryTriggerName(tableName));
        }


        // creates SQL to create history trigger
        SQLStatement sqlDropHistoryTrigger =
                new SQLStatement(this, SQLStatementType.DROP_HISTORY_TRIGGER);

        // makes keyword replacements
        sqlDropHistoryTrigger.replaceKeyword(
                ReplacementKeyword.TRIGGER_NAME, this.getHistoryTriggerName(tableName));


        // performs query
        sqlDropHistoryTrigger.executeUpdate();
    }

    /**
     * Returns PL/SQL code for saving historical row values.
     *
     * @param tableName table to be logged
     * @return PL/SQL code for saving historical values
     */
    protected String getHistoryTriggerLoop(String tableName) {
        List<String> columnNames =
                new ArrayList<String>(getNonLoggingColumns(tableName).keySet());

        // prepares list of column names
        String columnNamesList = Util.implode(
                Constant.COMMA, columnNames.toArray());

        // prepares list of column values
        List<String> columnValues = new ArrayList<String>();
        for (String columnName : columnNames) {
            columnValues.add(
                    getTriggerVariableEscapeChar() + SQLKeyword.OLD +
                    "." + columnName);
        }
        String columnValuesList =
                Util.implode(Constant.COMMA, columnValues.toArray());


        // prepares SQL statement
        SQLStatement sqlHistoryTriggerLoop = new SQLStatement(
                this, SQLStatementType.HISTORY_TRIGGER);

        // makes keyword replacements
        sqlHistoryTriggerLoop.replaceKeyword(
                ReplacementKeyword.COLUMN_NAMES, columnNamesList);
        sqlHistoryTriggerLoop.replaceKeyword(
                ReplacementKeyword.COLUMN_VALUES, columnValuesList);
        sqlHistoryTriggerLoop.replaceKeyword(
                ReplacementKeyword.TABLE_NAME, this.getHistoryTableName(tableName));
        sqlHistoryTriggerLoop.replaceKeyword(
                ReplacementKeyword.PREFIX, this.loggingObjectsPrefix);

        // sends result
        return sqlHistoryTriggerLoop.getRawSQL();
    }

    /**
     * Returns escape character for trigger variables (NEW, OLD).
     *
     * @return escape character for trigger variables (NEW, OLD)
     */
    protected abstract String getTriggerVariableEscapeChar();

    @Override
    public final void startLoggingHistory(String tableName) {
        try {
            // starts transaction
            this.conn.setAutoCommit(false);


            // creates new columns
            this.createHistoryColumns(tableName);

            // creates history table and trigger
            this.createHistoryTable(tableName);
            this.createHistoryTrigger(tableName);


            // commits transaction
            this.conn.commit();

            // sets auto commiting
            this.conn.setAutoCommit(true);
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public final void stopLoggingHistory(String tableName) {
        try {
            // starts transaction
            this.conn.setAutoCommit(false);

            // drops logging objects
            this.dropHistoryTrigger(tableName);
            this.dropHistoryTable(tableName);
            this.dropHistoryColumns(tableName);


            // commits transaction
            this.conn.commit();

            // sets auto commiting
            this.conn.setAutoCommit(true);
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public ResultSet getHistoricalTableContent(String tableName, DateTime dateTime) {
        // checks existance of the history table
        if (!this.hasHistoryTable(tableName)) {
            throw new HistoryLoggingInactive(tableName);
        }


        // creates SQL to return content of the table from the history
        SQLStatement sqlGetHistoricalTableContent =
                new SQLStatement(this, SQLStatementType.GET_HISTORICAL_TABLE_CONTENT);


        // prepares list of column names
        String columnNamesList = Util.implode(
                Constant.COMMA,
                getNonLoggingColumns(tableName).keySet().toArray());

        // makes keyword replacements
        sqlGetHistoricalTableContent.replaceKeyword(
                ReplacementKeyword.TABLE_NAME, tableName);
        sqlGetHistoricalTableContent.replaceKeyword(
                ReplacementKeyword.HISTORY_TABLE_NAME,
                this.getHistoryTableName(tableName));
        sqlGetHistoricalTableContent.replaceKeyword(
                ReplacementKeyword.PREFIX, this.loggingObjectsPrefix);
        sqlGetHistoricalTableContent.replaceKeyword(
                ReplacementKeyword.COLUMN_NAMES, columnNamesList);

        // prepares statement
        sqlGetHistoricalTableContent.prepareStatement();

        String dateTimeString = dateTime.toString();
        // sets the table name and the column name
        sqlGetHistoricalTableContent.setString(1, dateTimeString);
        sqlGetHistoricalTableContent.setString(2, dateTimeString);
        sqlGetHistoricalTableContent.setString(3, dateTimeString);

        // executes query
        ResultSet resultGetHistoricalTableContent =
                sqlGetHistoricalTableContent.executeQuery();


        // send result
        return resultGetHistoricalTableContent;
    }

    @Override
    public ResultSet getLoggingTableContent(Object[] tableNames, Object[] operations, Object[] userNames,
            DateTime dateTimeFrom, DateTime dateTimeTo) {
        // checks existance of the logging table
        if (!hasLoggingTable()) {
            throw new ObjectDoesNotExistException(
                    DatabaseObjectType.TABLE, getLoggingTableName());
        }


        // creates SQL to return content of the table from the history
        SQLStatement sqlGetLoggingTableContent =
                new SQLStatement(this, SQLStatementType.GET_LOGGING_TABLE_CONTENT);


        List<String> whereClauses = new ArrayList<String>();

        // prepares list of table names
        if (tableNames.length > 0) {
            whereClauses.add("    lower(table_name) IN (" +
                    Util.implode(Constant.COMMA, Util.quote(tableNames)) +
                    ")\n");
        }

        // prepares list of users
        if (userNames.length > 0) {
            whereClauses.add("    lower(user_name) IN (" +
                    Util.implode(Constant.COMMA, Util.quote(userNames)) +
                    ")\n");
        }

        // prepares list of operations
        if (operations.length > 0) {
            String[] operationChars = new String[operations.length];
            for (int i = 0; i < operations.length; ++i) {
                operationChars[i] = operations[i].toString().substring(0, 1).toUpperCase();
            }

            whereClauses.add("    operation IN (" +
                    Util.implode(Constant.COMMA, Util.quote(operationChars)) +
                    ")\n");
        }

        // prepares from and to
        if (dateTimeFrom != null) {
            whereClauses.add("    changed >= to_timestamp('" + dateTimeFrom.toString() + "', 'YYYY-MM-DD HH24:MI:SS')\n");
        }

        if (dateTimeTo != null) {
            whereClauses.add("    changed <= to_timestamp('" + dateTimeTo.toString() + "', 'YYYY-MM-DD HH24:MI:SS')\n");
        }


        // makes keyword replacements
        sqlGetLoggingTableContent.replaceKeyword(
                ReplacementKeyword.LOGGING_TABLE_NAME, getLoggingTableName());
        if (whereClauses.size() > 0) {
            sqlGetLoggingTableContent.replaceKeyword(
                    ReplacementKeyword.WHERE_CLAUSE,
                    "WHERE\n" + Util.implode("    AND \n", whereClauses.toArray()));
        } else {
            sqlGetLoggingTableContent.replaceKeyword(
                    ReplacementKeyword.WHERE_CLAUSE, "");
        }

        // executes query
        ResultSet resultGetLoggingTableContent =
                sqlGetLoggingTableContent.executeQuery();


        // send result
        return resultGetLoggingTableContent;
    }

    @Override
    public Collection<String> getUsers() {
        try {
            // creates SQL to get all users
            SQLStatement sqlGetUsers =
                    new SQLStatement(this, SQLStatementType.GET_USERS);

            // executes query
            ResultSet resultGetTables = sqlGetUsers.executeQuery();

            // retrieves list of tables
            Set<String> users = new HashSet<String>();
            while (resultGetTables.next()) {
                String userName = resultGetTables.getString(1);

                users.add(userName);
            }

            // closes statement
            sqlGetUsers.close();


            // send result
            return users;
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }

    @Override
    public Map<String, DatabaseObjectType> getLoggingObjects() {
        if (!hasPrefixFunction()) {
            return new HashMap<String, DatabaseObjectType>();
        }

        try {
            // creates SQL to get all users
            SQLStatement sqlGetLoggingObjects =
                    new SQLStatement(this, SQLStatementType.GET_LOGGING_OBJECTS);

            // makes replacements
            sqlGetLoggingObjects.replaceKeyword(ReplacementKeyword.PREFIX,
                    loggingObjectsPrefix);
            sqlGetLoggingObjects.replaceKeyword(ReplacementKeyword.PREFIX_FUNCTION_NAME,
                    getPrefixFunctionName());

            // executes query
            ResultSet resultGetLoggingObjects =
                    sqlGetLoggingObjects.executeQuery();

            // retrieves list of tables
            Map<String, DatabaseObjectType> loggingObjects =
                    new HashMap<String, DatabaseObjectType>();
            while (resultGetLoggingObjects.next()) {
                String objectName = resultGetLoggingObjects.getString(1);
                DatabaseObjectType objectType =
                        DatabaseObjectType.valueOf(
                        resultGetLoggingObjects.getString(2));

                loggingObjects.put(objectName, objectType);
            }

            // closes statement
            sqlGetLoggingObjects.close();


            // send result
            return loggingObjects;
        } catch (SQLException ex) {
            throw new DatabaseErrorException(ex);
        }
    }
}